package com.xuecheng.manage_media.service;

import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.config.RabbitMQConfig;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.print.attribute.standard.Media;
import java.io.*;
import java.util.*;

/**
 * 文件上传服务
 */
@Service
public class MediaUploadService {
    @Autowired
    MediaFileRepository mediaFileRepository;

    @Autowired
    RabbitTemplate rabbitTemplate;
    //上传文件根目录
    @Value("${xc-service-manage-media.upload-location}")
    private String uploadPath;
    // routeKey
    @Value("${xc-service-manage-media.mq.routingkey-media-video}")
    private String routeKey;

    /**
     * 文件上传前 注册, 检查文件是否存在
     *
     * @param fileMd5
     * @param fileName
     * @param fileSize
     * @param mimetype
     * @param fileExt
     * @return
     */
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 1. 检查文件在磁盘上是否存在
        // 获取文件在服务器的路径
        String fileFolderPath = this.getFilePath(fileMd5, fileExt);
        File file = new File(fileFolderPath);
        //文件是否存在
        boolean exists = file.exists();
        // 2. 检查文件信息在MongoDB中是否存在
        Optional<MediaFile> mediaFileOptional = mediaFileRepository.findById(fileMd5);
        // 3. 如果同时存在
        if (exists && mediaFileOptional.isPresent()) {
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        // 4. 如果不同时存在
        String folderPath = this.getFileFolderPath(fileMd5);
        File folderFile = new File(folderPath);
        if (!folderFile.exists()) {
            folderFile.mkdirs();
        }
        // 5. 响应结果
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 获取文件的路径
     * 规则:
     * 一级目录: md5的第一个字符
     * 二级目录: md5的第二个字符
     * 三级目录: md5的值
     * 文件名: md5 + 文件拓展名
     *
     * @param fileMd5
     * @return 文件的路径字符串
     */
    private String getFilePath(String fileMd5, String fileExt) {
        // 获取一级目录
        String firstDir = fileMd5.substring(0, 1);
        // 二级目录
        String secondDir = fileMd5.substring(1, 2);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(uploadPath).append(firstDir).append("/").append(secondDir).append("/").append(fileMd5).append("/")
                .append(fileMd5).append(".").append(fileExt);
        return stringBuilder.toString();
    }

    /**
     * 获取(文件所在文件夹)的路径
     *
     * @param fileMd5
     * @return
     */
    private String getFileFolderPath(String fileMd5) {
        // 获取一级目录
        String firstDir = fileMd5.substring(0, 1);
        // 二级目录
        String secondDir = fileMd5.substring(1, 2);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(uploadPath).append(firstDir).append("/").append(secondDir).append("/").append(fileMd5);
        return stringBuilder.toString();
    }

    /**
     * 获取(块文件)所在目录的路径
     *
     * @param fileMd5
     * @return
     */
    private String getChunkFileFolderPath(String fileMd5) {
        // 获取一级目录
        String firstDir = fileMd5.substring(0, 1);
        // 二级目录
        String secondDir = fileMd5.substring(1, 2);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(uploadPath).append(firstDir).append("/").append(secondDir)
                .append("/").append(fileMd5).append("/").append("chunk").append("/");
        return stringBuilder.toString();
    }

    /**
     * 获取文件所在目录的相对路径
     *
     * 如:  c/5/c5c75d70f382e6016d2f506d134eee11/
     * @param fileMd5
     * @param fileExt
     * @return
     */
    private String getFileRelativePath(String fileMd5, String fileExt) {
        // 获取一级目录
        String firstDir = fileMd5.substring(0, 1);
        // 二级目录
        String secondDir = fileMd5.substring(1, 2);
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(firstDir).append("/").append(secondDir).append("/").append(fileMd5).append("/");
        return stringBuilder.toString();
    }

    /**
     * 检查分块文件是否存在
     *
     * @param fileMd5
     * @param chunk     文件块的序号
     * @param chunkSize
     * @return
     */
    public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
        // 1. 获取块文件的目录
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        //定义块文件对象
        File file = new File(chunkFileFolderPath + chunk);
        // 2. 检查是否存在
        if (file.exists()) {
            return new CheckChunkResult(CommonCode.SUCCESS, true);
        } else {
            return new CheckChunkResult(CommonCode.SUCCESS, false);
        }
    }

    /**
     * 上传  文件块
     *
     * @param file    文件块
     * @param chunk   文件块序号
     * @param fileMd5
     * @return 上传结果
     */
    public ResponseResult uploadChunk(MultipartFile file, Integer chunk, String fileMd5) {
        // 1. 检查文件所在目录是否存在, 不存在就创建文件夹
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            chunkFileFolder.mkdirs();
        }
        // 2. 上传文件到服务器
        InputStream inputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            //获取上传文件输入流
            inputStream = file.getInputStream();
            //文件输出流
            fileOutputStream = new FileOutputStream(new File(chunkFileFolderPath + "/" + chunk));
            IOUtils.copy(inputStream, fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileOutputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 文件合并
     *
     * @param fileMd5  根据文件计算的md5值,对应MongoDB主键id
     * @param fileName  文件原始名称
     * @param fileSize
     * @param mimetype
     * @param fileExt  文件扩展名
     * @return
     */
    public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 1. 合并分块文件
        // 1.1 获取合并文件的路径
        String mergeFilePath = this.getFilePath(fileMd5, fileExt);
        File mergeFile = new File(mergeFilePath);
        if (mergeFile.exists()) {
            //如果合并文件已存在就先删除
            mergeFile.delete();
        }
        try {
            //创建新的合并文件
            mergeFile.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 1.2 获取分块文件所在文件夹
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        // 获取块文件列表
        File[] chunkFiles = chunkFileFolder.listFiles();
        List<File> chunkFileList = Arrays.asList(chunkFiles);
        // 进行排序
        chunkFileList.sort((o1, o2) -> {
            int i1 = Integer.parseInt(o1.getName());
            int i2 = Integer.parseInt(o2.getName());
            return i1 - i2;
        });
        // 1.3 合并文件
        mergeFile = this.merge(chunkFileList, mergeFile);
        if (mergeFile == null) {
            ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
        }
        // 2. 校验md5
        boolean b = this.checkFileMd5(mergeFile, fileMd5);
        if (!b) {
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }
        //3. 向mongoDB中保存信息
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileName(fileMd5 + "." + fileExt);
        mediaFile.setFileOriginalName(fileName);
            //文件保存路径   相对路径,同时不包含文件名  c/5/c5c75d70f382e6016d2f506d134eee11/
        mediaFile.setFilePath(this.getFileRelativePath(fileMd5, fileExt));
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);
            //状态为上传成功
        mediaFile.setFileStatus("301002");
        mediaFileRepository.save(mediaFile);
        //4. 合并完成后删除分块文件

        //5. 发送mq消息,处理上传的视频
        return this.sendMQ(fileMd5);
    }

    /**
     * 发送mq消息, 处理视频(转成mp4,m3u8)
     * @param mediaId 文件的id就是fileMd5值, 对应MongoDB数据库文档 主键
     * @return
     */
    public ResponseResult sendMQ(String mediaId) {
        // 1.先检验数据库中该文件是否存在
        Optional<MediaFile> mediaFileOptional = mediaFileRepository.findById(mediaId);
        if (!mediaFileOptional.isPresent()) {
            return new ResponseResult(CommonCode.FAIL);
        }
        // 2.定义消息
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("mediaId", mediaId);
        String jsonString = JSON.toJSONString(map);
        try {
            //发送消息
            rabbitTemplate.convertAndSend(RabbitMQConfig.EX_MEDIA_PROCESSTASK, routeKey, jsonString);
        } catch (AmqpException e) {
            e.printStackTrace();
            return new ResponseResult(CommonCode.FAIL);
        }
        return new ResponseResult(CommonCode.SUCCESS);
    }

    /**
     * 比较合并后文件的md5 和 前端传过来文件md5
     *
     * @param mergeFile
     * @param fileMd5   前端传过来文件md5
     * @return 比较结果
     */
    private boolean checkFileMd5(File mergeFile, String fileMd5) {
        if (StringUtils.isEmpty(fileMd5)) {
            return false;
        }
        // 进行md5校验
        try {
            //获取合并后文件的md5
            FileInputStream fileInputStream = new FileInputStream(mergeFile);
            String md5Hex = DigestUtils.md5Hex(fileInputStream);
            // 比较md5
            if (fileMd5.equalsIgnoreCase(md5Hex)) {
                return true;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 合并分块文件
     *
     * @param chunkFileList 已经排序的块文件
     * @param mergeFile     合并后的文件
     * @return 合并后的文件
     */
    private File merge(List<File> chunkFileList, File mergeFile) {
        RandomAccessFile raf_rw = null;
        RandomAccessFile raf_r = null;
        try {
            //文件输出流
            raf_rw = new RandomAccessFile(mergeFile, "rw");
            //遍历数组, 读取块文件
            for (File file : chunkFileList) {
                raf_r = new RandomAccessFile(file, "r");
                byte[] bytes = new byte[1024];
                int len = -1;
                while ((len = raf_r.read(bytes)) != -1) {
                    raf_rw.write(bytes, 0, len);
                }
                raf_r.close();
            }
            raf_rw.close();
            return mergeFile;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
